package han.Chensing.CCirnosLibrary.composer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Random;

import javax.crypto.CipherOutputStream;

import han.Chensing.CCirnosLibrary.CCirnoLibrary;
import han.Chensing.CCirnosLibrary.P;
import han.Chensing.CCirnosLibrary.map.ItemDecInfo;
import han.Chensing.CCirnosLibrary.map.ItemMap;
import han.Chensing.CCirnosLibrary.map.ItemNode;
import han.Chensing.CCirnosLibrary.util.AESUtil;
import han.Chensing.CCirnosLibrary.util.IOUtil;
import han.Chensing.CCirnosLibrary.util.MD5Util;

public class CCirnoLibraryComposer {
    private File srcPath;
    private File outPath;

    private final Random random;

    public CCirnoLibraryComposer() {
        this.random=new Random();
    }

    public CCirnoLibraryComposer(String srcPath, String outPath){
        this.srcPath =new File(srcPath);
        this.outPath=new File(outPath);
        this.random=new Random();
    }

    public void compose(){
        P.i("Deleting old file");
        File[] listFiles = outPath.listFiles();
        if (listFiles!=null)
            for(File file:listFiles)
                deleteAll(file);
        P.i("Building tree");
        ItemMap itemMap=new ItemMap();
        File[] files = srcPath.listFiles();
        if (files==null) return;
        ItemNode rootNode = itemMap.getRootNode();
        for (File file : files)
            compose(file, rootNode,"");
        itemMap.getRootNode().printMap();
        P.i("Outputting file");
        outputNode(rootNode,outPath,true);
        P.i("Generating index");
        genIndex(itemMap);
        P.i("Outputting libData file");
        try {
            File libDataFile = new File(outPath,"libData");
            FileOutputStream fileOutputStream=new FileOutputStream(libDataFile);
            ItemMap.toStream(itemMap,fileOutputStream);
            fileOutputStream.close();
        }catch (Exception e){
            P.e("Error in outputting libData file");
            e.printStackTrace();
        }
        P.i("Outputting .gitignore file");
        try{
            File gitIgnoreFile=new File(outPath,".gitignore");
            FileOutputStream fileOutputStream=new FileOutputStream(gitIgnoreFile);
            fileOutputStream.write("libData\n".getBytes(StandardCharsets.UTF_8));
            fileOutputStream.close();
        }catch (Exception e){
            P.e("Error in outputting .gitignore file");
            e.printStackTrace();
        }
        P.i("Done");
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void outputNode(ItemNode itemNode, File root,boolean isRootNode) {
        if (itemNode.isList()) {
            File nRoot = new File(root, itemNode.getName());
            if (!isRootNode)
                nRoot.mkdir();
            if (isRootNode)
                nRoot=root;
            for (ItemNode childNode : itemNode.getChildNodes())
                outputNode(childNode, nRoot,false);
            return;
        }
        try {
            boolean encrypted = itemNode.getItemFlag().containsFlag("encrypted");
            File fromFile = new File(srcPath, itemNode.getFakeLink());
            File outFile = new File(root, itemNode.getName());
            InputStream fileInputStream = new FileInputStream(fromFile);
            File tempFile = new File(root, itemNode.getName() + "$temp");
            if (!itemNode.getItemFlag().containsFlag("blocks")) {
                File nOutFile=outFile;
                if (encrypted)
                    nOutFile=tempFile;
                FileOutputStream fileOutputStream = new FileOutputStream(nOutFile);
                IOUtil.copyStream(fileInputStream, fileOutputStream);
                fileInputStream.close();
                fileOutputStream.close();
                if(encrypted) {
                    IOUtil.encryptFile(nOutFile, outFile, itemNode.getItemDecInfo());
                    nOutFile.delete();
                }
                P.i("Out :" + itemNode.getFakeLink());
            } else {
                if (encrypted){
                    fileInputStream.close();
                    IOUtil.encryptFile(fromFile,tempFile, itemNode.getItemDecInfo());
                    fileInputStream=new FileInputStream(tempFile);
                }
                File fileDir = new File(root, itemNode.getName());
                fileDir.mkdir();
                IOUtil.splitWrite(fileInputStream, fileDir);
                fileInputStream.close();
                if (encrypted)
                    tempFile.delete();
                P.i("OutS:" + itemNode.getFakeLink());
            }
        } catch (Exception e) {
            P.e("Fail:" + itemNode.getFakeLink());
        }
    }

    private void compose(File pathOrFile, ItemNode parentNode,String rootPath){
        String fileName = pathOrFile.getName();
        if (fileName.endsWith(".cLibD")|| fileName.endsWith("indexCLibD"))
            return;
        File decFile=new File(pathOrFile.getAbsolutePath()+".cLibD");
        if (pathOrFile.isDirectory()) {
            String name = MD5Util.getMD5OfString(parentNode.getName());
            if (name==null)
                name=random.nextInt()+"";
            ItemNode itemNode=new ItemNode();
            itemNode.setParent(parentNode);
            itemNode.setName(name);
            itemNode.setFakeLink(getFakeLink(pathOrFile));
            itemNode.setOriginalName(pathOrFile.getName());
            itemNode.setFolder(true);
            applyDecInfo(itemNode,decFile);
            parentNode.getChildNodes().add(itemNode);
            File[] files = pathOrFile.listFiles();
            if (files==null)
                return;
            String newRootPath=getLink(rootPath,name);
            for (File file : files)
                compose(file,itemNode,newRootPath);
            return;
        }
        String name = MD5Util.getMD5OfString(pathOrFile.getName());
        if (name==null)
            name=random.nextInt() + "";
        ItemNode itemNode=new ItemNode();
        itemNode.setFileMD5(MD5Util.getMD5OfFile(pathOrFile));
        itemNode.setOriginalName(pathOrFile.getName());
        itemNode.setName(name);
        itemNode.setLink(getLink(rootPath,name));
        itemNode.setFakeLink(getFakeLink(pathOrFile));
        itemNode.setParent(parentNode);
        if (pathOrFile.length()>=1023*1024L)
            itemNode.getItemFlag().addFlag("blocks");
        applyDecInfo(itemNode,decFile);
        parentNode.getChildNodes().add(itemNode);
    }

    private String getLink(String rootPath,String name){
        String newRootPath;
        if (rootPath.isEmpty())
            newRootPath=name;
        else
            newRootPath=rootPath+"/"+name;
        return newRootPath;
    }

    private String getFakeLink(File file){
        return file.getAbsolutePath().substring(srcPath.getAbsolutePath().length()+1);
    }

    private void applyDecInfo(ItemNode node,File decFile){
        if (!decFile.exists()) {
            ItemDecInfo itemDecInfo = node.getParent().getItemDecInfo();
            if (itemDecInfo==null)
                return;
            node.setItemDecInfo(itemDecInfo);
            node.getItemFlag().addFlag("encrypted");
            return;
        }
        try {
            String s = IOUtil.readFirstLineFromFile(decFile);
            ItemDecInfo itemDecInfo=new ItemDecInfo();
            itemDecInfo.setP(s);
            node.setItemDecInfo(itemDecInfo);
            node.getItemFlag().addFlag("encrypted");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private OutputStream decChangeOutputStream(ItemNode node,OutputStream outputStream){
        ItemDecInfo itemDecInfo = node.getItemDecInfo();
        if (itemDecInfo==null)
            return outputStream;
        try {
            return AESUtil.wrapOutputStream(outputStream, itemDecInfo.getP());
        }catch (Exception e){
            e.printStackTrace();
            return outputStream;
        }
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void deleteAll(File root){
        if (root.isDirectory()) {
            File[] files = root.listFiles();
            if (files==null){
                root.delete();
                return;
            }
            for (File file : files)
                deleteAll(file);
            root.delete();
            return;
        }
        root.delete();
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void genIndex(ItemMap map){
        try{
            File indexEFile=new File(srcPath,"indexCLibD");
            boolean encrypted = indexEFile.exists();
            File indexFile= new File(outPath,"index");
            PrintWriter writer=new PrintWriter(indexFile);
            writer.println(CCirnoLibrary.INDEX_MAGIC);
            map.getRootNode().query((queryType, node, parent) -> {
                if (node.isRootNode())
                    return;
                switch (queryType){
                    case MenuIn:
                        writer.println("#>"+node.getOriginalName());
                        break;
                    case MenuOut:
                        writer.println("#<");
                        break;
                    case Node:
                        writer.print("&&&");
                        Iterator<String> iterator = node.getItemFlag().getFlags().iterator();
                        while (iterator.hasNext()){
                            String next = iterator.next();
                            writer.print(next);
                            if (iterator.hasNext())
                                writer.print("|");
                        }
                        writer.print("&&&");
                        writer.print("$#");
                        writer.print(node.getLink());
                        writer.print("$#");
                        writer.print("<-<");
                        writer.print(node.getOriginalName());
                        writer.print(">->");
                        writer.print("~`");
                        writer.print(node.getOriginalName());
                        writer.print("`~");
                        writer.println();
                        break;
                }
            });
            writer.close();
            if (encrypted){
                String p = IOUtil.readFirstLineFromFile(indexEFile);
                File encryptedIndexFile=new File(outPath,"eIndex");
                FileOutputStream fileOutputStream=new FileOutputStream(encryptedIndexFile);
                FileInputStream fileInputStream=new FileInputStream(indexFile);
                CipherOutputStream outputStream = AESUtil.wrapOutputStream(fileOutputStream, p);
                IOUtil.copyStream(fileInputStream,outputStream);
                outputStream.close();
                fileOutputStream.close();
                fileInputStream.close();
                indexFile.delete();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public File getSrcPath() {
        return srcPath;
    }

    public void setSrcPath(File srcPath) {
        this.srcPath = srcPath;
    }

    public File getOutPath() {
        return outPath;
    }

    public void setOutPath(File outPath) {
        this.outPath = outPath;
    }
}
